using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text;
using Wilson.ORMapper;
using NWrapper.Config;

namespace NWrapper.Repositories.WilsonORMapper
{
	/// <summary>
	/// Repository implementation for Paul Wilson's O/R Mapper
	/// (see http://code.google.com/p/wilsonormapper).
	/// </summary>
	/// <remarks>
	/// On initialization, an <see cref="ORMapperInterceptor"/> is hooked for logging purposes.
	/// </remarks>
	public class WilsonORMapperRepository : IRepository, IDisposable
	{
		private ObjectSpace _objectSpace;
        private bool _disposed = false;

		#region Properties
        /// <summary>
        /// Gets the object space for the O/R mapper.
        /// </summary>
        /// <value>The object space.</value>
		protected ObjectSpace ObjectSpace
		{
			get
			{
				if (_objectSpace == null)
					throw new RepositoryException("Repository has not been initialized.");

				return _objectSpace;
			}
		}
		#endregion

		#region Constructors and Finalizers
        /// <summary>
        /// Initializes a new instance of the <see cref="WilsonORMapperRepository"/> class.
        /// </summary>
		public WilsonORMapperRepository()
			: base()
		{
		}

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="WilsonORMapperRepository"/> is reclaimed by garbage collection.
        /// </summary>
        ~WilsonORMapperRepository()
        {
            Dispose(false);
        }
		#endregion

		#region Private Methods
		private ObjectSpace GetObjectSpace(ConnectionStringSettings connectionStringSettings, Stream mappingsFile)
		{
			CustomProvider provider;
			string providerName = connectionStringSettings.ProviderName;
			string connectionString = connectionStringSettings.ConnectionString;

			#region Parse providername to get objectSpace
			switch (providerName)
			{
				case "System.Data.SqlClient":
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, Provider.Sql2005, 20, 5);
					break;
				case "System.Data.OleDb":
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, Provider.OleDb, 20, 5);
					break;
				case "System.Data.Odbc":
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, Provider.Odbc, 20, 5);
					break;
				case "System.Data.OracleClient":
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, Provider.Oracle, 20, 5);
					break;
				case "MySql.Data":
					provider = new CustomProvider(
						"MySql.Data",
						"MySql.Data.MySqlClient.MySqlConnection",
						"MySql.Data.MySqlClient.MySqlDataAdapter");
					provider.StartDelimiter = "`";
					provider.EndDelimiter = "`";
					provider.ParameterPrefix = "?";
					provider.IdentityQuery = "SELECT LAST_INSERT_ID()";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "ByteFX.MySqlClient":
					provider = new CustomProvider(
						"ByteFX.MySqlClient",
						"ByteFX.Data.MySqlClient.MySqlConnection",
						"ByteFX.Data.MySqlClient.MySqlDataAdapter");
					provider.StartDelimiter = "`";
					provider.EndDelimiter = "`";
					provider.IdentityQuery = "SELECT LAST_INSERT_ID()";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "CoreLab.MySql":
					provider = new CustomProvider(
						"CoreLab.MySql",
						"CoreLab.MySql.MySqlConnection",
						"CoreLab.MySql.MySqlDataAdapter");
					provider.StartDelimiter = "`";
					provider.EndDelimiter = "`";
					provider.IdentityQuery = "SELECT LAST_INSERT_ID()";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "Npgsql":
					provider = new CustomProvider(
						"Npgsql",
						"Npgsql.NpgsqlConnection",
						"Npgsql.NpgsqlDataAdapter");
					provider.StartDelimiter = "\"";
					provider.EndDelimiter = "\"";
					provider.ParameterPrefix = ":";
					provider.IdentityQuery = "SELECT currval('{1}_{0}_seq')";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "Finisar.SQLite":
					provider = new CustomProvider(
						"Finisar.SQLite",
						"Finisar.SQLite.SQLiteConnection",
						"Finisar.SQLite.SQLiteDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT last_insert_rowid()";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "FirebirdSql.Data.Firebird":
					provider = new CustomProvider(
						"FirebirdSql.Data.Firebird",
						"FirebirdSql.Data.Firebird.FbConnection",
						"FirebirdSql.Data.Firebird.FbDataAdapter");
					provider.StartDelimiter = "\"";
					provider.EndDelimiter = "\"";
					provider.IdentityQuery = "SELECT gen_id(gen_{1}_id, 0) FROM RDB$DATABASE";
					provider.SelectPageQuery = "SELECT FIRST {0} SKIP {1} *";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "IBM.Data.DB2":
					provider = new CustomProvider(
						"IBM.Data.DB2",
						"IBM.Data.DB2.DB2Connection",
						"IBM.Data.DB2.DB2DataAdapter");
					provider.StartDelimiter = "`";
					provider.EndDelimiter = "`";
					provider.IdentityQuery = "VALUES IDENTITY_VAL_LOCAL()";
					provider.SelectPageQuery = null;
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "VistaDB":
					provider = new CustomProvider(
						"VistaDB.Provider",
						"VistaDB.VistaDBConnection",
						"VistaDB.VistaDBDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT LastIdentity({0}) FROM {1}";
					provider.SelectPageQuery = "SELECT TOP {2}, {0} *";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "Sybase.Data.AseClient":
					provider = new CustomProvider(
						"Sybase.Data.AseClient",
						"Sybase.Data.AseClient.AseConnection",
						"Sybase.Data.AseClient.AseDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT @@IDENTITY";
					provider.SelectPageQuery = null;
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "Mono.Data.SybaseClient":
					provider = new CustomProvider(
						"Mono.Data.SybaseClient",
						"Mono.Data.SybaseClient.SybaseConnection",
						"Mono.Data.SybaseClient.SybaseDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT @@IDENTITY";
					provider.SelectPageQuery = null;
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "Wilson.XmlDbClient":
					provider = new CustomProvider(
						"WilsonXmlDbClient",
						"Wilson.XmlDbClient.XmlDbConnection",
						"Wilson.XmlDbClient.XmlDbDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT @@Identity;";
					provider.SelectPageQuery = "SELECT * LIMIT {0} OFFSET {1}";
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				case "System.Data.SqlServerCe":
					provider = new CustomProvider(
						"System.Data.SqlServerCe",
						"System.Data.SqlServerCe.SqlCeConnection",
						"System.Data.SqlServerCe.SqlCeDataAdapter");
					provider.StartDelimiter = "[";
					provider.EndDelimiter = "]";
					provider.IdentityQuery = "SELECT SCOPE_IDENTITY()";
					provider.SelectPageQuery = null;
					_objectSpace = new ObjectSpace(mappingsFile, connectionString, provider, 20, 5);
					break;
				default:
					throw new ConfigurationErrorsException("WormProvider ProviderName from connection string is invalid.");
			}
			#endregion

			return _objectSpace;
		}
		private ConnectionStringSettings GetConnectionStringSettings(string key)
		{
			ConnectionStringSettings settings = ConfigurationManager.ConnectionStrings[key];
			if (settings == null)
				throw new ConfigurationErrorsException("Could not find connection string element with the key " + key + ".");

			return settings;
		}
		#endregion

		#region IDisposable Members
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
		public void Dispose()
		{
            Dispose(true);
            GC.SuppressFinalize(this);
		}

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        private void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_objectSpace != null)
                    {
                        _objectSpace.Dispose();
                        _objectSpace = null;
                    }
                }
                _disposed = true;
            }
        }
		#endregion

		#region IRepository Members
		/// <summary>
		/// Initializes the repository by invoking the underlying Wilson.ORMapper instance.
		/// </summary>
		/// <param name="settings">The settings to use for initialization.</param>
		public void Initialize(RepositorySettings settings)
		{
			//check settings
			KeyValueConfigurationElement connectionString = settings.Custom["connectionString"] as KeyValueConfigurationElement;
			KeyValueConfigurationElement mappingsFileName = settings.Custom["mappingsFileName"] as KeyValueConfigurationElement;

			if (connectionString == null)
				throw new ConfigurationErrorsException("WilsonORMapperRepositoryRepository requires connectionString custom setting");
			if (mappingsFileName == null)
				throw new ConfigurationErrorsException("WilsonORMapperRepositoryRepository requires mappingsFileName custom setting");

			//do initialization
			string connStrKey = connectionString.Value;
			ConnectionStringSettings connStr = GetConnectionStringSettings(connStrKey);

			string fullname = Path.Combine(Environment.CurrentDirectory, mappingsFileName.Value);
			using (Stream stream = File.OpenRead(fullname))
			{
				_objectSpace = GetObjectSpace(connStr, stream);
			}

			if (_objectSpace == null)
				throw new RepositoryException("Unable to initialize WilsonORMapperRepository.");

			IInterceptCommand interceptor = new ORMapperInterceptor();
			_objectSpace.SetInterceptor(interceptor);
		}

		private IList<T> InnerRetrieve<T>(string whereClause, string sortClause, int pageSize, int pageIndex)
		{
			ObjectQuery<T> query = new ObjectQuery<T>(whereClause, sortClause, pageSize, pageIndex);
			return ObjectSpace.GetObjectSet<T>(query);
		}
		private T InnerRetrieveFirst<T>(string whereClause, string sortClause)
		{
			OPathQuery<T> query = new OPathQuery<T>(whereClause, sortClause);
			return ObjectSpace.GetObject<T>(query);
		}

		/// <summary>
		/// Finds the first instance of a given type.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <returns>An object instance, or null if not found.</returns>
		public T FindFirst<T>()
		{
			return InnerRetrieveFirst<T>(String.Empty, String.Empty);
		}

		/// <summary>
		/// Finds all instances of a given type.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <returns>An collection of object instance. If no objects are found, an empty collection is returned.</returns>
		public IList<T> FindAll<T>()
		{
			return InnerRetrieve<T>(String.Empty, String.Empty, int.MaxValue, 1);
		}

		/// <summary>
		/// Saves an object to the repository.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <param name="target">The object to save.</param>
		public void Save<T>(T target)
		{
			if (target == null)
				throw new ArgumentNullException("target");

			ObjectSpace.PersistChanges(target, PersistDepth.SingleObject);
		}

		/// <summary>
		/// Deletes an object from the repository.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <param name="target">The object to delete.</param>
		public void Delete<T>(T target)
		{
			if (target == null)
				throw new ArgumentNullException("target");

			ObjectSpace.MarkForDeletion(target);
			ObjectSpace.PersistChanges(target);
		}

		/// <summary>
		/// Deletes all objects of a given type from the repository.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		public void DeleteAll<T>()
		{
			ObjectSpace.ExecuteDelete<T>(String.Empty);
		}

		/// <summary>
		/// Reloads an object from the repository.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <param name="target">The object to reload.</param>
		/// <returns>The reloaded object.</returns>
		public T Resync<T>(T target)
		{
			if (target == null)
				throw new ArgumentNullException("target");

			return (T)ObjectSpace.Resync(target);
		}

		/// <summary>
		/// Gets the total number of records of a given type.
		/// </summary>
		/// <typeparam name="T">The object type.</typeparam>
		/// <returns>The total number of records.</returns>
		public int CountAll<T>()
		{
			return ObjectSpace.GetObjectCount<T>(String.Empty);
		}
		#endregion
	}
}
